package com.ikingtech.platform.service.pay.controller;

import com.ikingtech.framework.sdk.base.model.BatchParam;
import com.ikingtech.framework.sdk.context.exception.FrameworkException;
import com.ikingtech.framework.sdk.core.response.R;
import com.ikingtech.framework.sdk.enums.pay.PayStatusEnum;
import com.ikingtech.framework.sdk.enums.pay.PayTypeEnum;
import com.ikingtech.framework.sdk.pay.api.PayApi;
import com.ikingtech.framework.sdk.pay.embedded.CashierBuilder;
import com.ikingtech.framework.sdk.pay.embedded.properties.PayProperties;
import com.ikingtech.framework.sdk.pay.embedded.supplier.*;
import com.ikingtech.framework.sdk.pay.model.*;
import com.ikingtech.framework.sdk.utils.Tools;
import com.ikingtech.framework.sdk.web.annotation.ApiController;
import com.ikingtech.platform.service.pay.entity.CashierSupplierDO;
import com.ikingtech.platform.service.pay.entity.PayRecordDO;
import com.ikingtech.platform.service.pay.exception.PayExceptionInfo;
import com.ikingtech.platform.service.pay.service.CashierSupplierService;
import com.ikingtech.platform.service.pay.service.PayRecordService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author tie yan
 */
@Slf4j
@RequiredArgsConstructor
@ApiController(value = "/pay", name = "支付中心", description = "支付中心")
public class PayController implements PayApi {

    private final CashierBuilder cashierBuilder;

    private final PayRecordService recordService;

    private final CashierSupplierService supplierService;

    private final PayProperties properties;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public R<PayOrderDTO> order(PaymentDTO payment) {
        CashierSupplierConfig config = this.loadConfig(payment.getSupplierId());
        PayRecordDO payingEntity = this.recordService.getByRequestIdAndStatus(payment.getRequestId(), PayStatusEnum.PAYING.name());
        if (null != payingEntity) {
            return R.ok(Tools.Json.toBean(payingEntity.getProps(), PayOrderDTO.class));
        }
        PayRecordDO entity = Tools.Bean.copy(payment, PayRecordDO.class);
        entity.setId(Tools.Id.uuid());
        entity.setSupplierType(config.getType().name());
        entity.setStatus(PayStatusEnum.PAYING.name());
        PayOrder payOrder = this.cashierBuilder.build(config)
                .pay(PayArgs.builder()
                        .payType(payment.getPayType())
                        .amount(payment.getAmount())
                        .requestId(payment.getRequestId())
                        .subject(payment.getTitle())
                        .channelUserId(payment.getChannelUserId())
                        .channelAppId(payment.getChannelAppId())
                        .option(payment.getOption())
                        .build()
                );
        entity.setChannelTradeId(payOrder.getChannelTradeId());
        entity.setSupplierTradeId(payOrder.getSupplierTradeId());
        entity.setProps(Tools.Json.toJsonStr(payOrder));
        this.recordService.save(entity);
        return R.ok(Tools.Bean.copy(payOrder, PayOrderDTO.class));
    }

    @Override
    public R<Object> refund(PayRefundParamDTO refundParam) {
        PayRecordDO entity = this.recordService.getByRequestIdAndStatus(refundParam.getRequestId(), PayStatusEnum.PAY_SUCCESS.name());
        if (null == entity) {
            throw new FrameworkException("successPayRecordNotFound");
        }
        RefundResult result = this.cashierBuilder
                .build(this.loadConfig(entity.getSupplierId()))
                .refund(RefundArgs.builder()
                        .requestId(entity.getRequestId())
                        .refundAmount(refundParam.getAmount())
                        .totalAmount(entity.getAmount())
                        .build());
        entity.setStatus(Boolean.TRUE.equals(result.getSuccess()) ? PayStatusEnum.REFUNDED.name() : PayStatusEnum.REFUNDED_FAIL.name());
        entity.setCause(result.getCause());
        this.recordService.updateById(entity);
        return R.ok();
    }

    @Override
    public R<Object> cancel(PaymentCancelParamDTO cancelParam) {
        PayRecordDO entity = this.recordService.getByRequestIdAndStatus(cancelParam.getRequestId(), PayStatusEnum.PAYING.name(), PayStatusEnum.PAY_SUCCESS.name());
        if (null == entity) {
            throw new FrameworkException(PayExceptionInfo.PAY_RECORD_NOT_FOUND);
        }
        CancelResult result = this.executePayCancel(entity);
        entity.setStatus(Boolean.TRUE.equals(result.success()) ? PayStatusEnum.PAY_CANCEL.name() : PayStatusEnum.PAY_CANCEL_FAIL.name());
        entity.setCause(result.getCause());
        this.recordService.updateById(entity);
        return R.ok();
    }

    @Override
    public R<Object> redPack(PayRedPackDTO redPack) {
        CashierSupplierConfig cashierSupplierConfig = this.loadConfig(redPack.getSupplierId());
        PayRecordDO entity = Tools.Bean.copy(redPack, PayRecordDO.class);
        entity.setId(Tools.Id.uuid());
        entity.setSupplierType(cashierSupplierConfig.getType().name());
        RedPackArgs args = RedPackArgs.builder()
                .requestId(redPack.getRequestId())
                .amount(redPack.getAmount())
                .receiverCount(redPack.getReceiverCount())
                .title(redPack.getTitle())
                .wishingWords(redPack.getWishingWords())
                .channelUserId(redPack.getChannelUserId())
                .remark(redPack.getRemark())
                .build();
        RedPackResult result = this.cashierBuilder.build(cashierSupplierConfig).redPack(args);
        entity.setSupplierTradeId(result.getSupplierTradeId());
        entity.setChannelTradeId(result.getChannelTradeId());
        if (!Boolean.TRUE.equals(result.getSending())) {
            entity.setStatus(result.success() ? PayStatusEnum.RED_PACK_SUCCESS.name() : PayStatusEnum.RED_PACK_FAIL.name());
            entity.setCause(result.getCause());
        } else {
            entity.setStatus(PayStatusEnum.RED_PACK_SENDING.name());
        }
        entity.setCause(result.getCause());
        entity.setProps(Tools.Json.toJsonStr(args));
        this.recordService.save(entity);
        return R.ok();
    }

    @Override
    public R<PayRecordDTO> getPayResultByRequestId(String requestId) {
        List<PayRecordDO> entities = this.recordService.listByRequestId(requestId);
        if (Tools.Coll.isBlank(entities)) {
            throw new FrameworkException(PayExceptionInfo.PAY_RECORD_NOT_FOUND);
        }
        PayRecordDTO payRecord = Tools.Bean.copy(entities.get(0), PayRecordDTO.class);
        payRecord.setSupplierTypeName(payRecord.getSupplierType().description);
        payRecord.setPayTypeName(payRecord.getPayType().description);
        payRecord.setStatusName(payRecord.getStatus().description);
        return R.ok(payRecord);
    }

    @Override
    public R<List<PayRecordDTO>> listPayResultByRequestIds(BatchParam<String> requestIds) {
        List<PayRecordDO> entities = this.recordService.listByRequestIds(requestIds.getList());
        if (Tools.Coll.isBlank(entities)) {
            throw new FrameworkException(PayExceptionInfo.PAY_RECORD_NOT_FOUND);
        }
        List<PayRecordDTO> result = new ArrayList<>();
        Tools.Coll.convertGroup(entities, PayRecordDO::getRequestId).forEach((requestId, recordEntities) -> {
            PayRecordDTO payRecord = Tools.Bean.copy(recordEntities.get(0), PayRecordDTO.class);
            if (null != payRecord.getSupplierType()) {
                payRecord.setSupplierTypeName(payRecord.getSupplierType().description);
            }
            if (null != payRecord.getPayType()) {
                payRecord.setPayTypeName(payRecord.getPayType().description);
            }
            if (null != payRecord.getStatus()) {
                payRecord.setStatusName(payRecord.getStatus().description);
            }
            result.add(payRecord);
        });

        return R.ok(result);
    }

    @RequestMapping("/notify/{requestId}")
    public String payResultNotify(@PathVariable String requestId,
                                  @RequestBody(required = false) String payResultBody,
                                  @RequestParam(required = false) Map<String, Object> payResultMap) {
        PayRecordDO entity = this.recordService.getByRequestIdAndStatus(requestId, PayStatusEnum.PAYING.name());
        if (null == entity) {
            return Tools.Str.EMPTY;
        }
        PayResult result = this.cashierBuilder.build(this.loadConfig(entity.getSupplierId())).validate(payResultBody, payResultMap);
        if (!PayStatusEnum.PAYING.name().equals(entity.getStatus())) {
            return result.getSupplierResponse();
        }
        this.updatePayRecord(entity, result);
        this.recordService.updateById(entity);
        return result.getSupplierResponse();
    }

    @Scheduled(fixedDelay = 1, timeUnit = TimeUnit.MINUTES)
    public void pollingPayResult() {
        List<PayRecordDO> payingRecordEntities = this.recordService.listByStatus(PayStatusEnum.PAYING.name());
        if (Tools.Coll.isNotBlank(payingRecordEntities)) {
            for (PayRecordDO payingRecordEntity : payingRecordEntities) {
                if (LocalDateTime.now().minusMinutes(20).isAfter(payingRecordEntity.getCreateTime())) {
                    CancelResult result = this.executePayCancel(payingRecordEntity);
                    payingRecordEntity.setStatus(Boolean.TRUE.equals(result.success()) ? PayStatusEnum.PAY_CANCEL.name() : PayStatusEnum.PAY_CANCEL_FAIL.name());
                    payingRecordEntity.setCause(result.getCause());
                } else {
                    PayResult result = this.cashierBuilder
                            .build(this.loadConfig(payingRecordEntity.getSupplierId()))
                            .getPayResult(GetPayResultArgs.builder()
                                    .requestId(payingRecordEntity.getRequestId())
                                    .supplierTradeId(payingRecordEntity.getSupplierTradeId())
                                    .channelTradeId(payingRecordEntity.getChannelTradeId())
                                    .build());
                    if (Boolean.TRUE.equals(result.getPaying())) {
                        continue;
                    }
                    this.updatePayRecord(payingRecordEntity, result);
                }
            }
            this.recordService.updateBatchById(payingRecordEntities);
        }
    }

    @Scheduled(fixedDelay= 1, timeUnit = TimeUnit.HOURS)
    public void pollingRedPackResult() {
        List<PayRecordDO> sendingRecordEntities = this.recordService.listByStatus(PayStatusEnum.RED_PACK_SENDING.name());
        if (Tools.Coll.isNotBlank(sendingRecordEntities)) {
            sendingRecordEntities.forEach(entity -> {
                RedPackArgs args = RedPackArgs.builder()
                        .requestId(entity.getRequestId())
                        .build();
                RedPackResult result = this.cashierBuilder.build(this.loadConfig(entity.getSupplierId())).readPackConfirm(args);
                if (!Boolean.TRUE.equals(result.getSending())) {
                    entity.setStatus(result.success() ? PayStatusEnum.RED_PACK_SUCCESS.name() : PayStatusEnum.RED_PACK_FAIL.name());
                }
                entity.setCause(result.getCause());
            });
            this.recordService.updateBatchById(sendingRecordEntities);
        }
    }

    private CashierSupplierConfig loadConfig(String supplierId) {
        CashierSupplierDO entity = this.supplierService.getById(supplierId);
        return Tools.Bean.copy(Objects.requireNonNullElseGet(entity, () -> this.getFromProperties(supplierId)), CashierSupplierConfig.class);
    }

    private PayProperties.Supplier getFromProperties(String supplierId) {
        for (PayProperties.Supplier supplier : this.properties.getSuppliers()) {
            if (supplierId.equals(supplier.getId())) {
                return supplier;
            }
        }
        throw new FrameworkException(PayExceptionInfo.CASHIER_SUPPLIER_NOT_FOUND);
    }

    private CancelResult executePayCancel(PayRecordDO entity) {
        return this.cashierBuilder
                .build(this.loadConfig(entity.getSupplierId()))
                .cancel(CancelArgs.builder()
                        .requestId(entity.getRequestId())
                        .amount(entity.getAmount())
                        .payType(PayTypeEnum.valueOf(entity.getPayType()))
                        .build());
    }

    private void updatePayRecord(PayRecordDO entity, PayResult result) {
        entity.setSupplierTradeId(result.getSupplierTradeId());
        entity.setChannelTradeId(result.getChannelTradeId());
        entity.setTradeTime(result.getTradeTime());
        entity.setStatus(Boolean.TRUE.equals(result.getSuccess()) ? PayStatusEnum.PAY_SUCCESS.name() : PayStatusEnum.PAY_FAILED.name());
        entity.setCause(result.getCause());
    }
}
